﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics.Contracts;
using System.Linq;
using System.Text;
using System.Reflection;


namespace EnumUtils
{
  public static class FlagEnumUtilities
  {
    /// <summary>
    /// Bit shifts the flag enum bit pattern leftward.
    /// </summary>
    /// 
    /// <param name="flagEnum">Flag enum instance to bit-shift on (is NOT modified)</param>
    /// 
    /// <param name="timesToShift">The number of times to shift the bit pattern.</param>
    /// 
    /// <returns></returns>
    /// 
    /// <exception cref="ArgumentException">
    /// Thrown when bitPattern is not a Flags enum.
    /// </exception>
    public static TEnum
    BitShiftLeft<TEnum>( TEnum flagEnum, int timesToShift )
      where TEnum : IConvertible {
//TODO: Add direct unit tests to this method and BitShiftRight.
      Contract.Requires<ArgumentNullException>(!object.Equals(flagEnum, null));
      Contract.Requires<ArgumentException>(flagEnum is System.Enum);
      Contract.Requires<ArgumentException>(!flagEnum.GetType().IsAbstract, "This method is not contravariant. Please pass in a concrete enum type.");
      Contract.Requires<ArgumentException>(FlagEnumUtilities.DoesEnumHaveFlagsAttribute(flagEnum), ErrorMessageUtils.EnumNotAFlagsEnumMessage);


      ulong enumAsAnInt = Convert.ToUInt64(flagEnum);
//FIXME: Both here and in other places, I assume that the size of an enum is an in, when it can very well be anything from a sbyte to an ulong. I need to write code that accounts for this and fix all instances throughout my codebase.
      enumAsAnInt <<= timesToShift;
      var intToEnumBitmask = FlagEnumUtilities.GenerateBitmask(flagEnum);

      enumAsAnInt &= intToEnumBitmask;
      TEnum result = FlagEnumUtilities.ConvertIntToEnum<TEnum, ulong>(enumAsAnInt);

      return result;
    }


    /// <summary>
    /// Bit shifts the flag enum bit pattern rightward.
    /// </summary>
    /// 
    /// <param name="flagEnum"></param>
    /// 
    /// <param name="timesToShift">The number of times to shift the bit pattern.</param>
    /// 
    /// <returns></returns>
    public static TEnum
    BitShiftRight<TEnum>( TEnum flagEnum, int timesToShift )
      where TEnum : IConvertible {
      Contract.Requires<ArgumentNullException>(!object.Equals(flagEnum, null));
      Contract.Requires<ArgumentException>(flagEnum is System.Enum);
      Contract.Requires<ArgumentException>(!flagEnum.GetType().IsAbstract, "This method is not contravariant. Please pass in a concrete enum type.");
      Contract.Requires<ArgumentException>(FlagEnumUtilities.DoesEnumHaveFlagsAttribute(flagEnum), ErrorMessageUtils.EnumNotAFlagsEnumMessage);

      ulong enumAsAnInt = Convert.ToUInt64(flagEnum);
      enumAsAnInt >>= timesToShift;
      // RJM: Question: what if timesToShift was negative?
      TEnum result = FlagEnumUtilities.ConvertIntToEnum<TEnum,ulong>(enumAsAnInt);

      return result;
    }


    public static BitArray ConvertEnumToBitArray<TEnum>( TEnum flagEnum )
      where TEnum : IConvertible {
      Contract.Requires<ArgumentNullException>(!object.Equals(flagEnum, null));
      Contract.Requires<ArgumentException>(flagEnum is System.Enum);
      Contract.Requires<ArgumentException>(!flagEnum.GetType().IsAbstract, "This method is not contravariant. Please pass in a concrete enum type.");

      Type enumFlagsUnderlyingType = Enum.GetUnderlyingType(flagEnum.GetType());
      var enumFlagsBits_integer = Convert.ToUInt64(Convert.ChangeType(flagEnum, enumFlagsUnderlyingType));
//BUG_BAIT: Using ulong as a generic hold-all for the underlying value of an enum is begging to be thrown into signed vs. unsigned hell. Write a unit test to catch this!
      BitArray enumFlagsBits = new BitArray(BitConverter.GetBytes(enumFlagsBits_integer));
      enumFlagsBits.Length = (int)Math.Ceiling(Math.Log(GenerateBitmask(flagEnum), 2));
      
      return enumFlagsBits;
    }


    public static IEnumerable<TEnum> ConvertFlagEnumToIEnumerable<TEnum>( TEnum flagEnum )
      where TEnum : IConvertible {
      Contract.Requires<ArgumentNullException>(!object.Equals(flagEnum, null));
      Contract.Requires<ArgumentException>(flagEnum is System.Enum);
      Contract.Requires<ArgumentException>(!flagEnum.GetType().IsAbstract, "This method is not contravariant. Please pass in a concrete enum type.");
      Contract.Requires<ArgumentException>(DoesEnumHaveFlagsAttribute(flagEnum), ErrorMessageUtils.EnumNotAFlagsEnumMessage);

      IEnumerator<TEnum> flagEnumEnumerator = GetGenericEnumerator(flagEnum);

      while (flagEnumEnumerator.MoveNext()) {
        yield return flagEnumEnumerator.Current;
      }
    }



    /// <summary>
    /// Converts an int to enum.
    /// </summary>
    /// <typeparam name="TEnum">The type of the enum.</typeparam>
    /// <typeparam name="TInt">The type of the int.</typeparam>
    /// <param name="enumAsAnInt">The enum as an int.</param>
    /// <returns></returns>
    /// <exception cref="OverflowException">
    /// An OverflowException is thrown if enumAsAnInt is too big for the 
    /// target enum to represent.
    /// </exception>
    public static TEnum
        ConvertIntToEnum<TEnum, TInt>( TInt enumAsAnInt )
      where TInt : struct, IConvertible, IFormattable
      where TEnum : IConvertible {
      Contract.Requires<ArgumentException>(typeof(TEnum).IsSubclassOf(typeof(Enum)));

      TypeCode underylyingTypeCodeOfEnum = Type.GetTypeCode(Enum.GetUnderlyingType(typeof(TEnum)));
      object boxedEnumAsAnInt;
        // RJM: I box the enumAsAnInt to convert it at runtime to an integer 
       //      sized no larger than the enum we're converting to can 
       //      handle w/o using C# 4.0 specific code (like casting to 
       //      dynamic).


      switch (underylyingTypeCodeOfEnum) {
        case TypeCode.SByte:
          boxedEnumAsAnInt = SByte.Parse(enumAsAnInt.ToString("x", null), System.Globalization.NumberStyles.HexNumber);
          // this statement and others similar to it converts to a variably-typed integer based on raw bit values
          //LATER: Consider abstracting this code for ints in general and putting in a general library (strictly for your personal use)
          break;
        case TypeCode.Int16:
          boxedEnumAsAnInt = Int16.Parse(enumAsAnInt.ToString("x", null), System.Globalization.NumberStyles.HexNumber);
          break;
        case TypeCode.Int32:
          boxedEnumAsAnInt = Int32.Parse(enumAsAnInt.ToString("x", null), System.Globalization.NumberStyles.HexNumber);
          break;
        case TypeCode.Int64:
          boxedEnumAsAnInt = Int64.Parse(enumAsAnInt.ToString("x", null), System.Globalization.NumberStyles.HexNumber);
          break;
        case TypeCode.Byte:
          boxedEnumAsAnInt = Byte.Parse(enumAsAnInt.ToString("x", null), System.Globalization.NumberStyles.HexNumber);
          break;
        case TypeCode.UInt16:
          boxedEnumAsAnInt = UInt16.Parse(enumAsAnInt.ToString("x", null), System.Globalization.NumberStyles.HexNumber);
          break;
        case TypeCode.UInt32:
          boxedEnumAsAnInt = UInt32.Parse(enumAsAnInt.ToString("x", null), System.Globalization.NumberStyles.HexNumber);
          break;
        case TypeCode.UInt64:
          boxedEnumAsAnInt = UInt64.Parse(enumAsAnInt.ToString("x", null), System.Globalization.NumberStyles.HexNumber);
          break;
        case TypeCode.Boolean:
        case TypeCode.Char:
        case TypeCode.Single:
        case TypeCode.Decimal:
        case TypeCode.Double:
        case TypeCode.DBNull:
        case TypeCode.DateTime:
        case TypeCode.Empty:
        case TypeCode.Object:
        case TypeCode.String:
        default:
          throw new ArgumentException("This method expects enumAsAnInt to be an integral type");
      }

      return (TEnum)(Enum.Parse(typeof(TEnum), boxedEnumAsAnInt.ToString()));
    }


    [Pure]
    public static bool
    DoesEnumHaveFlagsAttribute<TEnum>( TEnum flagEnum )
      where TEnum : IConvertible {
      Contract.Requires<ArgumentNullException>(!object.Equals(flagEnum, null));
      Contract.Requires<ArgumentException>(flagEnum is System.Enum);
      Contract.Requires<ArgumentException>(!flagEnum.GetType().IsAbstract, "This method is not contravariant. Please pass in a concrete enum type.");
      
      bool hasFlagsAttribute = flagEnum.GetType().GetCustomAttributes(false).Any(flag => flag.GetType() == typeof(FlagsAttribute));
      return hasFlagsAttribute;
    }


    /// <summary>
    /// Performs an operation on each flag of a flags enumeration.
    /// </summary>
    /// 
    /// <param name="flagEnum">An enum with a Flags attribute to process.</param>
    /// 
    /// <param name="operation">An operation to execute on the passed enumeration. Must not attempt to do operations that you can't do to an enumeration (like bit-shifting).</param>
    public static void
    ForeachEnumFlag<TEnum>( TEnum flagEnum, Action<TEnum> operation )
      where TEnum : IConvertible {
      Contract.Requires<ArgumentNullException>(!object.Equals(flagEnum, null));
      Contract.Requires<ArgumentNullException>(!object.Equals(operation, default(Action<TEnum>)));
      Contract.Requires<ArgumentException>(flagEnum is System.Enum);
      Contract.Requires<ArgumentException>(!flagEnum.GetType().IsAbstract, "This method is not contravariant. Please pass in a concrete enum type.");
      Contract.Requires<ArgumentException>(DoesEnumHaveFlagsAttribute(flagEnum), ErrorMessageUtils.EnumNotAFlagsEnumMessage);

      BitArray enumFlagsBits = ConvertEnumToBitArray(flagEnum);
      for (int i = enumFlagsBits.Count - 1; i >= 0; --i) {
        ulong enumAsInteger = Convert.ToUInt64(enumFlagsBits[i]) << i;
        TEnum individualEnumFlag = (TEnum)Enum.Parse(flagEnum.GetType(), enumAsInteger.ToString());
        if (IsFlagDefined(individualEnumFlag)) {
          operation(individualEnumFlag);
        }
      }
    }



    /// <summary>
    /// Tests to determine if an enumeration flag is explicitly defined for an enumeration.
    /// </summary>
    /// 
    /// <param name="flagToTest"></param>
    /// 
    /// <param name="countNoneFlagAsFlag">
    /// Determines whether the None flag should count as a flag being 
    /// defined for the enum. Defaults to false.
    /// </param>
    /// 
    /// <returns></returns>
    /// 
    /// <remarks>
    /// Code based off of a code snippet found on pp. 95 in "C# 3.0 in a 
    /// Nutshell", written by Albahari, J &amp; Albahari, B.
    /// </remarks>
    public static bool
    IsFlagDefined<TEnum>( TEnum flagToTest, bool countNoneFlagAsFlag = false )
      where TEnum : IConvertible {
//TODO: Consolodate common unit tests into helper methods.
      Contract.Requires<ArgumentNullException>(!object.Equals(flagToTest, null));
      Contract.Requires<ArgumentException>(flagToTest is System.Enum);
      Contract.Requires<ArgumentException>(!flagToTest.GetType().IsAbstract, "This method is not contravariant. Please pass in a concrete enum type.");
      Contract.Requires<ArgumentException>(DoesEnumHaveFlagsAttribute(flagToTest), ErrorMessageUtils.EnumNotAFlagsEnumMessage);

      int dummyInt;
      if (int.TryParse(flagToTest.ToString(), out dummyInt)) {
        return false;
      }
      else {
        switch (flagToTest.ToString()) {
          case "None":
            return countNoneFlagAsFlag ? true : false;
          default:
              // Assume that any other value represents a valid flag
            return true;
        }
      }
    }



    /// <summary>
    /// Generates the bitmask.
    /// </summary>
    /// <typeparam name="TEnum">The type of the enum.</typeparam>
    /// <param name="flagEnum">The flag enum.</param>
    /// <returns>A ulong-sized bitmask that can be trimmed down to the appropriate size by casting appropriately</returns>
    public static ulong
    GenerateBitmask<TEnum>( TEnum flagEnum )
      where TEnum : IConvertible {
//TODO: Test me for signed vs unsigned issues.
      Contract.Requires<ArgumentNullException>(!object.Equals(flagEnum, null));
      Contract.Requires<ArgumentException>(flagEnum is System.Enum);
      Contract.Requires<ArgumentException>(!flagEnum.GetType().IsAbstract, "This method is not contravariant. Please pass in a concrete enum type.");
      Contract.Requires<ArgumentException>(DoesEnumHaveFlagsAttribute(flagEnum));

      var possibleEnumValues = Enum.GetValues(flagEnum.GetType());
      ulong maxEnumValue = Convert.ToUInt64(Convert.ChangeType(possibleEnumValues.GetValue(possibleEnumValues.GetUpperBound(0)), Enum.GetUnderlyingType(flagEnum.GetType())));
      ulong intToEnumBitmask = 1UL;
      // This is the value of 2^0

      while (intToEnumBitmask <= maxEnumValue) {
        intToEnumBitmask <<= 1;
      }
      --intToEnumBitmask;


      return intToEnumBitmask;
    }



    /// <summary>
    /// Gets an enumerator for a flags enumeration that enumerates through all flag values.
    /// </summary>
    /// 
    /// <param name="flagEnum"></param>
    /// 
    /// <returns></returns>
    public static IEnumerator
    GetEnumerator<TEnum>( TEnum flagEnum )
      where TEnum : IConvertible {
        // RJM: There are no Code Contracts in this method because it simply 
        //      wraps GetGenericEnumerator(flagEnum), which has all the code 
        //      contracts needed. It passes all the regression tests.
      return GetGenericEnumerator(flagEnum);
    }


    /// <summary>
    /// Gets an enumerator for a flags enumeration that enumerates through all flag values.
    /// </summary>
    /// 
    /// <param name="flagEnum"></param>
    /// 
    /// <returns></returns>
    public static IEnumerator<TEnum>
    GetGenericEnumerator<TEnum>( TEnum flagEnum )
      where TEnum : IConvertible {
      Contract.Requires<ArgumentNullException>(!object.Equals(flagEnum, null));
      Contract.Requires<ArgumentException>(flagEnum is System.Enum);
      Contract.Requires<ArgumentException>(!flagEnum.GetType().IsAbstract, "This method is not contravariant. Please pass in a concrete enum type.");
      Contract.Requires<ArgumentException>(DoesEnumHaveFlagsAttribute(flagEnum), ErrorMessageUtils.EnumNotAFlagsEnumMessage);

      BitArray enumBits = ConvertEnumToBitArray(flagEnum);
      for (int i = 0; i < enumBits.Length; ++i) {
        TEnum enumFlag = (TEnum)Enum.Parse(flagEnum.GetType(), Convert.ToUInt32(Convert.ToInt32(enumBits[i]) << i).ToString());
        if (IsFlagDefined(enumFlag)) {
          yield return enumFlag;
        }
      }
    }



    #region Helper Methods
    #endregion
  }
}
